
package com.whosly.mysql.protocol.packet.impl;

import com.whosly.mysql.protocol.FlagProtocolType;
import com.whosly.mysql.protocol.config.MySQLProtocolConfig;
import com.whosly.mysql.protocol.datatype.DataType;
import com.whosly.mysql.protocol.jdbc.MySQLMessage;
import com.whosly.mysql.protocol.packet.MysqlPackConstants;
import com.whosly.mysql.protocol.packet.jdbc.AbstractMySQLPacket;
import com.whosly.mysql.protocol.packet.jdbc.FieldsProtocol;
import com.whosly.mysql.protocol.packet.jdbc.FieldsProtocolType;
import com.whosly.mysql.protocol.packet.util.NettyBufferUtil;
import com.whosly.mysql.protocol.util.ByteUtils;
import com.whosly.mysql.protocol.util.CharsetUtil;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.NonNull;
import lombok.Setter;
import lombok.ToString;
import org.apache.commons.lang3.StringUtils;

/**
 * FieldPacket 字段包
 *
 * VERSION 4.1
 *
 * <pre>
 *     Bytes                      Name
 *  -----                      ----
 *  n (Length Coded String)    catalog
 *  n (Length Coded String)    db
 *  n (Length Coded String)    table
 *  n (Length Coded String)    org_table
 *  n (Length Coded String)    name
 *  n (Length Coded String)    org_name
 *  1                          (filler) (lenenc_int     length of fixed-length fields [0c])
 *  2                          character set
 *  4                          column length
 *  1                          type
 *  2                          flags
 *  1                          decimals
 *  2                          (filler), always 0x00
 *  n (Length Coded Binary)    default
 *
 * In practice, since identifiers are almost always 250 bytes or shorter, the Length Coded Strings look like: (1 byte for length of data) (data)
 *
 * catalog:                 Catalog. For 4.1, 5.0 and 5.1 the value is "def".
 * db:                      Database identifier, also known as schema name.
 * table:                   Table identifier, after AS clause (if any).
 * org_table:               Original table identifier, before AS clause (if any).
 * name:                    Column identifier, after AS clause (if any).
 * org_name:                Column identifier, before AS clause (if any).
 * charsetnr:               Character set number.
 * length:                  column Length of column, according to the definition.
 *                          Also known as "display length". The value given
 *                          here may be larger than the actual length, for
 *                          example an instance of a VARCHAR(2) column may
 *                          have only 1 character in it.
 *
 * type:
 *      @see  FieldsProtocolType
 *
 * flags:
 *      @see  FlagProtocolType
 *
 * decimals:                The number of positions after the decimal
 *                          point if the type is DECIMAL or NUMERIC.
 *                          Also known as "scale".
 * default:                 For table definitions. Doesn't occur for
 *                          normal result sets. See mysql_list_fields().
 * </pre>
 *
 *
 * @see  FieldsProtocol
 * @see  FieldsProtocolType
 *
 * @author fengyang(yueny09@163.com)
 * @date 2023年06月25日 下午2:33
 */
public class FieldPacket extends AbstractMySQLPacket
{

    private static final byte[] DEFAULT_CATALOG = MysqlPackConstants.DEFAULT_CATALOG.getBytes();
    private static final byte OC_BYTES = (byte) 0x0C;

    // 目录名称：在4.1及之后的版本中，该字段值为"def"。
    public byte[] catalog = DEFAULT_CATALOG;

    /**
     * 数仓名, 无法独立存在. 实际存在于 db 之前. 实际响应 db值 等于 warehouseName + MysqlPackConstants.DOT + db
     */
    private String warehouseName;
    // 数据库名称标识
    private String db;

    // 数据表的别名（AS之后的名称）
    public byte[] table;

    // 数据表的原始名称（AS之前的名称）。
    public byte[] orgTable;

    // 列（字段）的别名（AS之后的名称）。
    public byte[] name;

    // 列（字段）的原始名称（AS之前的名称）。
    public byte[] orgName;

    // 列（字段）的字符编码值。
    // character:
    // CharsetUtil.getIndex("utf8")
    @Getter
    private int charsetIndex;
    @Getter
    private transient String charsetName;

    // 列（字段）的长度值，真实长度可能小于该值，例如VARCHAR(2)类型的字段实际只能存储1个字符。
    public long length;

    /**
     * 列（字段）标志：各标志位定义如下（参考源代码/include/mysql_com.h头文件中的宏定义）：
     *
     * @see  FlagProtocolType
     */
    public FlagProtocolType flag;

    public byte decimals;

    /**
     * 列（字段）默认值，COM_FIELD_LIST命令时使用
     */
    public byte[] definition;
    public String definitionStr;

    /**
     * type to protocolType
     * @see  FieldsProtocol
     * @see  FieldsProtocolType
     */
    public FieldsProtocolType protocolType;
    @Deprecated
    public int type;

    public transient DataType javaType;

    /**
     * Only if command was COM_FIELD_LIST, definition field was written. The value is always be null as 0xfb
     * See @
     * https://dev.mysql.com/doc/internals/en/com-query-response.html#packet-Protocol::ColumnDefinition41
     * https://dev.mysql.com/doc/internals/en/com-field-list-response.html
     */
    @Setter
    private transient boolean fieldListCommand;

    /**
     * 集群名称
     */
    private String clusterName;

    public FieldPacket() {
        setCharsetIndex(MySQLProtocolConfig.DEFAULT_CHARSET_INDEX);
    }

    /**
     *  把字节数组转变成FieldPacket
     *
     *
     *  n (Length Coded String)    catalog
     *  n (Length Coded String)    schema/db   (warehouseName + . + db)
     *  n (Length Coded String)    table
     *  n (Length Coded String)    org_table
     *  n (Length Coded String)    name
     *  n (Length Coded String)    org_name
     *  1                          (filler)
     *  2                          character set
     *  4                          column length
     *  1                          type
     *  2                          flags
     *  1                          decimals
     *  2                          (filler), always 0x00
     *  if command was COM_FIELD_LIST {
     *      lenenc_int     length of default-values
     *      string[$len]   default values
     *   }
     *
     */
    @Override
    public void read(byte[] bodyBytes) {
        MySQLMessage mm = new MySQLMessage(bodyBytes);

        readBody(this, mm);
    }

    @Override
    public ByteBuf writeBuf(ByteBuf buffer) {
        int size = calcPacketSize();
        NettyBufferUtil.writeUB3(buffer, size);
        buffer.writeByte(getPackId());

        writeBody(buffer);
        return buffer;
    }

    @Override
    public ByteBuf writeBuf() {
        ByteBuf buffer = Unpooled.buffer(calcPacketSize());

        writeBuf(buffer);

        return buffer;
    }

    @Override
    public int calcPacketSize() {
        int packLength = 1;

        packLength = (catalog == null ? 1 : NettyBufferUtil.getLength(catalog));
        packLength += NettyBufferUtil.getLength(getDbFull());
        packLength += (table == null ? 1 : NettyBufferUtil.getLength(table));
        packLength += (orgTable == null ? 1 : NettyBufferUtil.getLength(orgTable));
        packLength += (name == null ? 1 : NettyBufferUtil.getLength(name));
        packLength += (orgName == null ? 1 : NettyBufferUtil.getLength(orgName));
        packLength += 13;// 1+2+4+1+2+1+2
        // COM_FIELD_LIST
        if (fieldListCommand) {
            if (definition == null || definition.length == 0) {
                // 默认值为null时为0xfb
                packLength += 1;
            } else {
                packLength += NettyBufferUtil.getLength(definition);
            }
        }

        return packLength;
    }

    private void writeBody(ByteBuf buffer) {
        byte nullVal = 0;

        NettyBufferUtil.writeWithLength(buffer, catalog, nullVal);
        NettyBufferUtil.writeWithLength(buffer, getDbFull(), nullVal);
        NettyBufferUtil.writeWithLength(buffer, table, nullVal);
        NettyBufferUtil.writeWithLength(buffer, orgTable, nullVal);
        NettyBufferUtil.writeWithLength(buffer, name, nullVal);
        NettyBufferUtil.writeWithLength(buffer, orgName, nullVal);

        // write filler. length of fixed-length fields [0c]
        buffer.writeByte(OC_BYTES);
        NettyBufferUtil.writeUB2(buffer, charsetIndex);
        NettyBufferUtil.writeUB4(buffer, length);

        if(protocolType != null){
            int type = protocolType.getType();
            buffer.writeByte((byte) (type & 0xff));
        }else{
            buffer.writeByte((byte) (type & 0xff));
        }

        NettyBufferUtil.writeUB2(buffer, flag.getFlag());

        buffer.writeByte(decimals);

        buffer.writeBytes(FILLER2);

        // COM_FIELD_LIST
        if (fieldListCommand) {
            NettyBufferUtil.writeWithLength(buffer, definition, NULL_MARK);
        }

    }

    public void setCharsetIndex(int charsetIndex) {
        String charset = CharsetUtil.getCharset(charsetIndex);
        if (charset != null) {
            this.charsetIndex = charsetIndex;
            this.charsetName = charset;
        }
    }

    @Override
    public String getPacketInfo() {
        return "MySQL Field Packet";
    }

    public byte[] getWarehouseName() {
        return ByteUtils.getBytes(warehouseName);
    }

    public byte[] getDb() {
        if(db == null){
            db = "";
        }
        return ByteUtils.getBytes(db);
    }

    private byte[] getDbFull(){
        String warehouseName = this.warehouseName;
        if(warehouseName == null){
            warehouseName = "";
        }
        String db = this.db;
        if(db == null){
            db = "";
        }

        return ByteUtils.getBytes(warehouseName + MysqlPackConstants.DOT + db);
    }

    public void setWarehouseName(String warehouseName) {
        this.warehouseName = warehouseName;
    }

    public void setClusterName(String clusterName) {
        this.clusterName = clusterName;
    }

    public void setDb(String db) {
        this.db = db;
    }

    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    @Getter
    @Setter
    @ToString
    public static class FieldBean {
        @NonNull
        private String name;

        @NonNull
        private Long length;

        /**
         * int type = (byte) MysqlDefs.javaTypeMysql(DataType, false)
         */
        @NonNull
        private int type;

        @NonNull
        private DataType javaType;

        @Builder.Default
        private byte packetId = (byte)1;

        @Builder.Default
        private String warehouseName = "";

        @Builder.Default
        private String db = "";

        @Builder.Default
        private String table = "";

        @Builder.Default
        private String orgTable = "";

        @Builder.Default
        private FlagProtocolType flags = FlagProtocolType._13;

        @Builder.Default
        private byte decimals = (byte)1;

        @Builder.Default
        private int characterIndex = MySQLProtocolConfig.DEFAULT_CHARSET_INDEX;

    }

    private static void readBody(FieldPacket fieldPacket, MySQLMessage mm) {
        fieldPacket.catalog = mm.readBytesWithLength();
        // db
        readWarehouseName(fieldPacket, mm.readBytesWithLength());
        fieldPacket.table = mm.readBytesWithLength();
        fieldPacket.orgTable = mm.readBytesWithLength();
        fieldPacket.name = mm.readBytesWithLength();
        fieldPacket.orgName = mm.readBytesWithLength();

        // only for move position: filler
        mm.move(1);
        fieldPacket.charsetIndex = mm.readUB2();
        fieldPacket.length = mm.readUB4();

        // type
        byte protocolType = mm.read();
//        fieldPacket.type = typeByte & 0xff;
        fieldPacket.type = protocolType;
        fieldPacket.protocolType = FieldsProtocolType.of(protocolType);
        if(fieldPacket.protocolType != null){
            DataType dataType = DataType.ofProtocolType(fieldPacket.protocolType.getType());
            fieldPacket.javaType = dataType;
        }

        // flags
        int flags = mm.readUB2();
        fieldPacket.flag = FlagProtocolType.of(flags);

        // decimals
        fieldPacket.decimals = mm.read();

        // only for move position: (filler), always 0x00, length  2
        mm.move(FILLER2.length);
        if (mm.hasRemaining() && fieldPacket.calcPacketSize() < fieldPacket.getPacketLength()) {
            // COM_FIELD_LIST
            fieldPacket.definition = mm.readBytesWithLength();
            fieldPacket.definitionStr = new String(fieldPacket.definition);
        }
    }

    private static void readWarehouseName(FieldPacket fieldPacket, byte[] db) {
        String dbStr = new String(db);
        String[] arr = StringUtils.split(dbStr, MysqlPackConstants.DOT);

        if(arr == null){
            return;
        }
        if(arr.length == 1){
            fieldPacket.db = arr[0];
            return;
        }
        if(arr.length == 2){
            fieldPacket.warehouseName = arr[0];
            fieldPacket.db = arr[1];
            return;
        }
    }

    /**
     * 从 MySQLMessage 中构造 ResultSetHeaderPacket 对象
     */
    public static final FieldPacket of(final MySQLMessage mm) {
        FieldPacket fieldPacket = new FieldPacket();
        fieldPacket.setArgs(mm.readUB3(), mm.read());

        readBody(fieldPacket, mm);

        return fieldPacket;
    }

}